package gjj.algorithm.image;

import gjj.util.ImageUtils;
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint;
import org.opencv.core.Point;
import org.opencv.core.Rect;
import org.opencv.core.Scalar;
import org.opencv.imgproc.Imgproc;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ImageSplitter {

    //轮廓与该外接矩形的面积比，如果大于阈值，则认为轮廓已经很接近矩形，该轮廓可以被认为用于切图
    private static final double EXTENT_THRESHOLD = 0.95;

    //根据聂鹏的实验结果。对于一个区域，如果(短边/区域面积) < MIN_REGION_AREA_RATIO，这个区域需要被抹掉
    private static final double MIN_REGION_AREA_RATIO = 30;

    public static class ImageSplitResult {
        private final List<MatOfPoint> contours;
        private final Mat fillSmallRegionResult;
        private final Mat fillIrregularRegionResult;

        public ImageSplitResult(List<MatOfPoint> contours, Mat fillSmallRegionResult, Mat fillIrregularRegionResult) {
            this.contours = contours;
            this.fillSmallRegionResult = fillSmallRegionResult;
            this.fillIrregularRegionResult = fillIrregularRegionResult;
        }

        public List<MatOfPoint> getContours() {
            return contours;
        }

        public Mat getFillSmallRegionResult() {
            return fillSmallRegionResult;
        }

        public Mat getFillIrregularRegionResult() {
            return fillIrregularRegionResult;
        }
    }

    private static List<MatOfPoint> findOuterContours(Mat image) {
        List<MatOfPoint> contours = new ArrayList<>();
        Mat hierarchy = new Mat();
        Imgproc.findContours(image, contours, hierarchy, Imgproc.RETR_TREE, Imgproc.CHAIN_APPROX_SIMPLE);
        List<MatOfPoint> outerContours = new ArrayList<>(contours.size());
        for (int i = 0; i < contours.size(); ++i) {
            MatOfPoint contour = contours.get(i);
            //0~3分别表示后一个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引值
            //参考资料：https://blog.csdn.net/yuanren201/article/details/104418710
            double[] h = hierarchy.get(0, i);
            if (h[3] == 0.0) {
                //内嵌轮廓的索引值为0时，说明这个轮廓是最外围的
                outerContours.add(contour);
            }
        }
        return outerContours;
    }

    public static ImageSplitResult findSmallImageRegions(Mat src) {
        Mat grayImg = new Mat();
        Imgproc.cvtColor(src, grayImg, Imgproc.COLOR_BGR2GRAY);
        Core.normalize(grayImg, grayImg, 0, 255, Core.NORM_MINMAX);
        //预处理获得原始切图Mask
        Mat splitMask = new Mat();
        Imgproc.threshold(grayImg, splitMask, 220, 255, Imgproc.THRESH_BINARY);
        //给图像加上外围的白边，让涂黑的轮廓不会粘到图片边缘导致不能被识别
        Core.copyMakeBorder(splitMask, splitMask, 10, 10, 10, 10, Core.BORDER_CONSTANT, new Scalar(255));
        //第一次找出所有区域
        List<MatOfPoint> contours = findOuterContours(splitMask);
        //抹掉过小的区域
        for (MatOfPoint contour : contours) {
            Rect region = Imgproc.boundingRect(contour);
            double a = Math.min(src.width(), src.height()) / MIN_REGION_AREA_RATIO;
            double minArea = a * a;
            if (region.area() < minArea) {
                MatOfPoint points = new MatOfPoint(
                        region.tl(),
                        new Point(region.x + region.width, region.y),
                        region.br(),
                        new Point(region.x, region.y + region.height)
                );
                Imgproc.fillPoly(splitMask, Collections.singletonList(points), new Scalar(255, 255, 255));
            }
        }
        Mat fillSmallRegionResult = ImageUtils.getImageCopy(splitMask);
        //至多重复做一定次数非矩形区域填充，让其轮廓尽量与其外接矩形一致
        int turn = 0;
        double regionExtend = 0;
        while (turn < 5 && regionExtend < 0.95) {
            //填充非矩形区域
            double minExtent = EXTENT_THRESHOLD;
            contours = findOuterContours(splitMask);
            for (MatOfPoint contour : contours) {
                Rect rect = Imgproc.boundingRect(contour);
                double contourArea = Imgproc.contourArea(contour);
                double extent = contourArea / rect.area();
                minExtent = Math.min(minExtent, extent);
                if (extent > EXTENT_THRESHOLD) {
                    continue;
                }
                //填充外接矩形
                MatOfPoint points = new MatOfPoint(
                        rect.tl(),
                        new Point(rect.x + rect.width, rect.y),
                        rect.br(),
                        new Point(rect.x, rect.y + rect.height)
                );
                Imgproc.fillPoly(splitMask, Collections.singletonList(points), new Scalar(0, 0, 0));
            }
            regionExtend = minExtent;
//            System.out.printf("当前最小extend值%f\n", minExtent);
            turn++;
        }
        Mat fillIrregularRegionResult = ImageUtils.getImageCopy(splitMask);
        //最后找出所有轮廓，并根据聂鹏的实验结果，过滤掉太小的轮廓
        contours = findOuterContours(splitMask);
        List<MatOfPoint> filteredContours = new ArrayList<>(contours.size());
        for (MatOfPoint contour : contours) {
            boolean bigEnough;
            Rect region = Imgproc.boundingRect(contour);
            double area = region.area();
            if (area < 1000) {
                bigEnough = false;
            } else {
                double mxAspectRatio = Math.max((double) region.height / region.width, (double) region.width / region.height);
                if (mxAspectRatio > 10 && area < 2000) {
                    bigEnough = false;
                } else {
                    bigEnough = true;
                }
            }
            if (bigEnough) {
                filteredContours.add(contour);
            }
        }
        return new ImageSplitResult(filteredContours, fillSmallRegionResult, fillIrregularRegionResult);
    }
}
